fix(devcontainer): honour the documented install switches, stream setup output, and bound every install - #62
Merged
Conversation
…host Two host-side strips used `sed -i 's/\r$//' FILE`. That is a GNU spelling. BSD sed -- every macOS host -- reads the argument after `-i` as the backup suffix, so it consumes the expression and then treats the file as the script. It exits non-zero. Inline in `initializeCommand`, `2>/dev/null` swallowed the error, so the strip silently never happened. In `strip_crlf()`, `set -e` propagated it and would have taken the whole `initializeCommand` down with it -- the bootstrap failing before the container is even created. Either way this is the CRLF guard failing on hosts it exists to protect: a Windows checkout opened through a macOS host is exactly the case that produces CRLF and exactly the case where the strip did not run. `perl -i -pe` means the same thing on both host families, and perl is present on macOS and on every Linux distribution carrying git. Refs #59 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Ali S <amirali.sadrzadeh@gmail.com>
…e connection on it `postCreateCommand` was the object form, with two named entries: a CRLF strip and the setup script. That shape reads as "do this, then that". It is not what it does. An object-form lifecycle command runs its entries in PARALLEL, and buffers each entry's output until that entry exits. So the pair did neither of the two things it looked like it did. The strip raced post-create.sh instead of preceding it -- both started in the same millisecond, and whether the scripts were stripped before they were sourced was a coin toss no one had noticed winning. And the setup entry printed nothing for the several minutes a cold tool install takes. Minutes of total silence from a container that is provisioning normally is indistinguishable from a deadlock. It sent someone looking for one that was not there, which is what makes this a defect rather than a preference: the output was being withheld precisely when it was the only evidence available. One string joined with `&&` fixes both halves. The ordering is now real, and a string command streams line by line as it runs. `waitFor` moves to `onCreateCommand` for the other half of the same complaint. post-create still runs to completion and still reports failure; it no longer holds the VS Code connection while it does. The trade-off is stated in the file rather than left for someone to discover: a terminal opened in the first couple of minutes will not have `task`, `lefthook` or `claude` on PATH yet. `remoteEnv` already points PATH at the mise shim directory, so they appear in the next shell -- no reload, no reconnect. Refs #59 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Ali S <amirali.sadrzadeh@gmail.com>
`.env.example` offered MUSHER_INSTALL_CLAUDE and MUSHER_INSTALL_CODEX as the
way to "skip the AI CLI installs on a slow connection", and devcontainer.json
declared both in containerEnv. Nothing read either one:
$ git grep -n MUSHER_INSTALL -- .devcontainer/
.env.example:29:# MUSHER_INSTALL_CLAUDE=0
.env.example:30:# MUSHER_INSTALL_CODEX=0
devcontainer.json:64: "MUSHER_INSTALL_CLAUDE": "1",
devcontainer.json:65: "MUSHER_INSTALL_CODEX": "1"
Four lines describing a behaviour, and no fifth line implementing it. Both CLIs
installed unconditionally, and they are most of the several minutes a cold
container spends in postCreateCommand -- so the one switch a developer on a slow
connection would reach for was the one that did nothing. MUSHER_LOG_LEVEL was
the same defect in miniature: documented in the same block, read by nothing.
`install_wanted()` reads the switch and `debug()` implements the log level.
Claude Code defaults ON as this repository's harness; Codex defaults OFF,
because it is an npm package carrying a platform binary and no one should pay
for a tool they do not use on every rebuild. Codex leaves the resolved set via
MISE_DISABLE_TOOLS rather than by editing mise.toml, so the pin stays recorded
and enabling it stays a one-line change to a known version.
The pins come out of containerEnv, and this is the half that would otherwise
have made the fix cosmetic. containerEnv becomes `docker run -e`, and an
explicit -e outranks --env-file for the same name whichever order they appear
in. Pinned to "1" there, they would have overridden the .devcontainer/.env that
.env.example tells developers to edit -- two places claiming to decide this, and
the documented one losing. Defaulting in the scripts leaves .env as the single
place that decides, and a comment says so where the pins used to be.
Verification follows the same rule: it now checks the tools this container was
asked to install, because reporting a missing `codex` to the developer who
switched it off is reporting a failure they asked for.
Refs #59
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Ali S <amirali.sadrzadeh@gmail.com>
…r a missing tool Two ways a cold build ended badly, neither of them the fault of the thing that went wrong. Every network install ran unbounded -- `curl | sh` for mise, `mise install`, `curl | bash` for Claude Code. A stalled connection parks postCreateCommand for as long as the kernel keeps the socket open, which is effectively forever, and the developer has no output to tell them apart from a container that is simply slow. `bounded()` wraps `timeout --foreground`, and the curls grow `--connect-timeout 10 --max-time 120`. A stall now fails, retries, and eventually reports -- "hangs forever" becomes "failed, and you can still work". The ceilings sit above the whole pipeline, not the fetch of the script: install.sh downloads a platform binary of its own after it is fetched. Separately, `base_verify_tools` ran last under `set -e`, so one missing CLI ended post-create.sh right there -- skipping install_lefthook_hooks and install_spec_tools, which is the git hooks and `bun install`. A container that was missing one tool ended up missing its hooks and its dependencies too, and the developer learned about the second failure later than the first. That is a worse outcome than the tool being missing warranted. Verification now reports through its return code. The repo-specific steps run, the status is carried to post-create.sh's exit code, and a final line names the condition rather than leaving the developer to scroll for a ✗. The container is still reported as half-provisioned, because it is -- it is just half-provisioned with working git hooks. Refs #59 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Ali S <amirali.sadrzadeh@gmail.com>
justinmerrell
approved these changes
Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes
Nothing about the document contract. This is
.devcontainer/only — it closesthe six cold-build defects catalogued in #59, one commit each for the four
distinct causes: the host-side CRLF strip was written in GNU
sedand failedsilently on every macOS host;
postCreateCommandwas the object form, whichruns its entries in parallel and buffers their output, so the strip raced the
setup script and a cold install printed nothing for minutes;
MUSHER_INSTALL_*and
MUSHER_LOG_LEVELwere documented in.env.exampleand read by no script,while
containerEnvpinned two of them so the documented.envopt-out couldnot have won anyway; and every network install ran unbounded, with
base_verify_toolsabortingpost-create.shunderset -eso a single missingCLI also cost the container its git hooks and its
bun install.Why
Closes #59. The dev container is the supported environment — CONTRIBUTING.md
sends every contributor to Reopen in Container and offers nothing else — so
these are defects in the one path a new contributor is guaranteed to take. No
ADR: nothing structural changes.
Compatibility
Checklist
task checkpasses locallygit commit -s)schemas/dist/— no schema touchedspec.mdaffectedNotes for the reviewer
Two changes are worth a second look because they are trade-offs rather than
straight fixes, and both are commented in place:
waitFormoves toonCreateCommand. Provisioning no longer gates the VSCode connection.
post-create.shstill runs to completion and still reportsfailure through its exit code. The cost: a terminal opened in the first couple
of minutes will not have
task,lefthookorclaudeonPATHyet.remoteEnvalready pointsPATHat the mise shim directory, so they appearin the next shell — no reload, no reconnect.
carrying a platform binary and it dominates the cold install; Claude Code is
this repository's harness. Codex leaves the resolved set via
MISE_DISABLE_TOOLSrather than by editingmise.toml, so the pin staysrecorded and re-enabling it is a one-line change to a known version. Anyone
who wants it back sets
MUSHER_INSTALL_CODEX=1in.devcontainer/.env.Verification was also narrowed to the tools this container was asked to
install — reporting a missing
codexto the developer who switched it off isreporting a failure they requested.
Testing
task checkpasses locally (exit 0), includingcheck:shell, whichShellChecks exactly the files this touches. Worth stating plainly: no check in
this repository covers the lifecycle configuration, and the failures live on
host families and cold paths CI never takes — which is why none of these six
were caught. The end-to-end confirmation is a rebuild without cache, ideally
one from a macOS host for the
perlchange.🤖 Generated with Claude Code